// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { MetadataBearer } from '@aws-sdk/types';
import {
	HttpResponse,
	parseJsonError as parseAwsJsonError,
} from '@aws-amplify/core/internals/aws-client-utils';
import { ApiErrorResponse } from '@aws-amplify/core/internals/utils';

import { RestApiError } from '../errors';

/**
 * Parses both AWS and non-AWS error responses coming from the users' backend code.
 * * AWS errors generated by the AWS services(e.g. API Gateway, Bedrock). They can be Signature errors,
 *   ClockSkew errors, etc. These responses will be parsed to errors with proper name and message from the AWS
 *   services.
 * * non-AWS errors thrown by the user code. They can contain any headers or body. Users need to access the
 *   error.response to get the headers and body and parse them accordingly. The JS error name and message will
 *   be `UnknownError` and `Unknown error` respectively.
 */
export const parseRestApiServiceError = async (
	response?: HttpResponse,
): Promise<(RestApiError & MetadataBearer) | undefined> => {
	if (!response) {
		// Response is not considered an error.
		return;
	}
	const parsedAwsError = await parseAwsJsonError(stubErrorResponse(response));
	if (!parsedAwsError) {
		// Response is not considered an error.
	} else {
		const bodyText = await response.body?.text();

		return buildRestApiError(parsedAwsError, {
			statusCode: response.statusCode,
			headers: response.headers,
			body: bodyText,
		});
	}
};

/**
 * The response object needs to be stub here because the parseAwsJsonError assumes the response body to be valid JSON.
 * Although this is true for AWS services, it is not true for responses from user's code. Once the response body is
 * unwrapped as JSON(and fail), it cannot be read as text again. Therefore, we need to stub the response body here to
 * make sure we can read the error response body as a JSON, and may fall back to read as text if it is not a valid JSON.
 */
const stubErrorResponse = (response: HttpResponse): HttpResponse => {
	let bodyTextPromise: Promise<string> | undefined;
	const bodyProxy = new Proxy(response.body, {
		get(target, prop, receiver) {
			if (prop === 'json') {
				// For potential AWS errors, error parser will try to parse the body as JSON first.
				return async () => {
					if (!bodyTextPromise) {
						bodyTextPromise = target.text();
					}
					try {
						return JSON.parse(await bodyTextPromise!);
					} catch (error) {
						// If response body is not a valid JSON, we stub it to be an empty object and eventually parsed
						// as an unknown error
						return {};
					}
				};
			} else if (prop === 'text') {
				// For non-AWS errors, users can access the body as a string as a fallback.
				return async () => {
					if (!bodyTextPromise) {
						bodyTextPromise = target.text();
					}

					return bodyTextPromise;
				};
			} else {
				return Reflect.get(target, prop, receiver);
			}
		},
	});
	const responseProxy = new Proxy(response, {
		get(target, prop, receiver) {
			if (prop === 'body') {
				return bodyProxy;
			} else {
				return Reflect.get(target, prop, receiver);
			}
		},
	});

	return responseProxy;
};

/**
 * Utility to create a new RestApiError from a service error.
 */
const buildRestApiError = (
	error: Error & MetadataBearer,
	response?: ApiErrorResponse,
): RestApiError & MetadataBearer => {
	const restApiError = new RestApiError({
		name: error?.name,
		message: error.message,
		underlyingError: error,
		response,
	});

	// $metadata is only required for backwards compatibility.
	return Object.assign(restApiError, { $metadata: error.$metadata });
};
